package org.particles.domain.sim;

import java.util.ArrayList;
import java.util.List;

import org.particles.common.CommonUtil;
import org.particles.domain.AbstractParticle;
import org.particles.domain.Particle;
import org.particles.domain.Statistic;
import org.particles.domain.StatisticItem;
import org.particles.domain.Vector2D;
import org.particles.domain.world.FieldRepository;
import org.particles.domain.world.RicochetOutOfBoundResolver;
import org.particles.domain.world.SimpleFieldRepository;
import org.particles.domain.world.World;
import org.particles.domain.world.World2D;

/**
 * basic simulation case for Wordl2D.
 */
public class AbstractSimulationCase implements SimulationCase {
	public static final int PARAM_WORLD_WIDTH = CommonUtil.uniqueIndex();
	public static final int PARAM_WORLD_HEIGHT = CommonUtil.uniqueIndex();
	/** number particles to be created */
	public static final int PARAM_NUM_PRTCL = CommonUtil.uniqueIndex();
	public static final int PARAM_PRTCL_RAD = CommonUtil.uniqueIndex();
	/** max particle impulse after creation */
	public static final int PARAM_MAX_IMPULSE = CommonUtil.uniqueIndex();
	
	private String name;
	private double worldWidth = 800;
	private double worldHeight = 600;
	private double numberParticles = 70;
	private boolean mustRecreate = true;
	private World2D world2D = new World2D();
	private double maxParticleImpulse = 1;
	private double particleRadius = 3;
	private FieldRepository fieldRepository = new SimpleFieldRepository();
	/**
	 * fields in which common particles will be embedded.
	 */
	private long fieldMarkers = 0;
	private List<Statistic> statistics = new ArrayList<Statistic>(5);
	private Statistic impulseStatistic = new Statistic("Impulse");
	
	public AbstractSimulationCase() {
		statistics.add(impulseStatistic);
	}

	public double getParticleRadius() {
		return particleRadius;
	}

	public double getMaxParticleImpulse() {
		return maxParticleImpulse;
	}

	public void setWorld(World2D world2d) {
		world2D = world2d;
	}

	/**
	 * @return fields in which common particles will be embedded.
	 */
	public long getFieldMarkers() {
		return fieldMarkers;
	}

	/**
	 * @param fields fields in which common particles will be embedded.
	 */
	public void setFieldMarkers(long fields) {
		this.fieldMarkers = fields;
	}

	public FieldRepository getFieldRepository() {
		return fieldRepository;
	}

	public void setFieldRepository(FieldRepository fieldRepository) {
		this.fieldRepository = fieldRepository;
	}

	public void setMaxParticleImpulse(final double maxParticleImpulse) {
		if(!this.mustRecreate) {
			this.maxParticleImpulse = maxParticleImpulse;
			for(Particle p : getWorld().getParticles()) {
				p.getImpulse().setLength(Math.random() * maxParticleImpulse);
			}
		}
		this.maxParticleImpulse = maxParticleImpulse;
	}

	public void setParticleRadius(final double particleRadius) {
		if(!this.mustRecreate) {
			this.particleRadius = particleRadius;
			for(Particle p : getWorld().getParticles()) {
				p.setRadius(particleRadius);
			}
		}
		this.particleRadius = particleRadius;
	}

	@Override
	public String getName() {
		return name;
	}
	
	@Override
	public World getWorld() {
		return world2D;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	public double getWorldWidth() {
		return worldWidth;
	}

	public void setWorldWidth(final double worldWidth) {
		if(this.worldWidth != worldWidth) {
			this.worldWidth = worldWidth;
			this.mustRecreate = true;
		}
	}

	public double getWorldHeight() {
		return worldHeight;
	}

	public void setWorldHeight(final double worldHeight) {
		if(this.worldHeight != worldHeight) {
			this.worldHeight = worldHeight;
			this.mustRecreate = true;
		}
	}

	public double getNumberParticles() {
		return numberParticles;
	}

	public void setNumberParticles(final double numberParticles) {
		final int numParticles = this.world2D.getParticles().size();
		if(!this.mustRecreate && numParticles != numberParticles) {
			// change number particles on the fly
			if(numberParticles > numParticles) {
				for(int i = 0; i < numberParticles - numParticles; i++) {
					addParticleToWorld();
				}
			}
			else if(numberParticles < numParticles) {
				removeParticlesFromWorld((int) (numParticles - numberParticles));
			}
		}
		this.numberParticles = numberParticles;
	}

	@Override
	public boolean changeParam(final int paramCode, final double newValue) {
		if(PARAM_WORLD_WIDTH == paramCode) {
			setWorldWidth(newValue);
			return true;
		}
		else if(PARAM_WORLD_HEIGHT == paramCode) {
			setWorldHeight(newValue);
			return true;
		}
		else if(PARAM_NUM_PRTCL == paramCode) {
			setNumberParticles(newValue);
			return true;
		}
		else if(PARAM_MAX_IMPULSE == paramCode) {
			setMaxParticleImpulse(newValue);
			return true;
		}
		else if(PARAM_PRTCL_RAD == paramCode) {
			setParticleRadius(newValue);
			return true;
		}
		else {
			return false;
		}
	}
	

	@Override
	public List<SimParam> availableParams() {
		final List<SimParam> params = new ArrayList<SimParam>(9);
		SimParam param;
		param = new SimParam(PARAM_WORLD_WIDTH, "world width", getWorldWidth());
		param.setMinValue(10D);
		param.setMaxValue(2000D);
		params.add(param);
		param = new SimParam(PARAM_WORLD_HEIGHT, "world height", getWorldHeight());
		param.setMinValue(10D);
		param.setMaxValue(2000D);
		params.add(param);
		param = new SimParam(PARAM_NUM_PRTCL, "number particles", getNumberParticles());
		param.setMinValue(2D);
		param.setMaxValue(1000D);
		params.add(param);
		param = new SimParam(PARAM_MAX_IMPULSE, 
				"max impulse", 
				getMaxParticleImpulse());
		param.setMinValue(Double.MIN_VALUE);
		param.setMaxValue(100D);
		params.add(param);
		param = new SimParam(PARAM_PRTCL_RAD, 
				"particle radius", 
				getParticleRadius());
		param.setMinValue(Double.MIN_VALUE);
		param.setMaxValue(100D);
		params.add(param);
		return params;
	}

	@Override
	public boolean mustRecreate() {
		return this.mustRecreate;
	}
	
	public void setMustRecreate(final boolean recreate) {
		this.mustRecreate = recreate;
	}
	
	/**
	 * adds a common particle to the world with max impulse 1.
	 */
	protected void addParticleToWorld() {
		getWorld().addParticle(createParticle());
	}
	
	protected AbstractParticle createParticle() {
		AbstractParticle particle = new AbstractParticle();
		Vector2D place = new Vector2D(
				Math.random() * getWorldWidth(),
				Math.random() * getWorldHeight());
		Vector2D impulse = new Vector2D(
				Math.random() * getMaxParticleImpulse() * createRandomSign(),
				Math.random() * getMaxParticleImpulse() * createRandomSign());
		particle.setSpacePlace(place);
		particle.setImpulse(impulse);
		particle.setRadius(getParticleRadius());
		particle.embedIntoFields(getFieldMarkers());
		return particle;
	}
	
	@Override
	public World createWorld() {
		world2D = new World2D();
		world2D.setDimension(new Vector2D(getWorldWidth(), getWorldHeight()));
		world2D.setOutOfBoundResolver(new RicochetOutOfBoundResolver());
		world2D.setFieldRepository(this.fieldRepository);
		for(int i = 0; i < getNumberParticles(); i++) {
			addParticleToWorld();
		}
		setMustRecreate(false);
		return world2D;
	}
	
	public void clearStatistic() {
		for(Statistic statistic : statistics) {
			statistic.clearStatistic();
		}
	}

	protected void removeParticlesFromWorld(final int numberParticles) {
		this.world2D.removeParticles(numberParticles);
	}

	protected double createRandomSign() {
		if(Math.random() > 0.5) {
			return -1;
		}
		else {
			return 1;
		}
	}

	@Override
	public String toString() {
		return getName();
	}

	@Override
	public List<Statistic> getStatistics() {
		return this.statistics;
	}

	@Override
	public synchronized void updateStatistics() {
		// impulse statistic
		double avg = 0;
		double min = Double.MAX_VALUE;
		double max = Double.MIN_VALUE;
		for(final Particle particle : this.world2D.getParticles()) {
			final double impulseScalar = particle.getImpulse().length();
			avg += impulseScalar;
			if(min > impulseScalar) {
				min = impulseScalar;
			}
			if(max < impulseScalar) {
				max = impulseScalar;
			}
		}
		avg = avg / (double) this.world2D.getParticles().size();
		impulseStatistic.addStatisticItem(new StatisticItem(avg, min, max));
	}

}
